package Shale;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;

import lombok.extern.slf4j.Slf4j;

/**
 * 单日执行流程模板
 *
 * @author leiwenkang <leiwenkang@kuaishou.com>
 * Created on 2020-10-24
 */
@Slf4j
public abstract class AbstractProcessor {

    public void process(ShaleContext context) {
        context.getDemandList().add(context.getThisDemand());
        calculateSupplyList(context);
        calculateSatisfyDemandMap(context);
        calSupplyAndTheta(context);

        context.getDemandList().remove(context.getThisDemand());
        //分配1次,计算beta
        allocate(context, true);
        doProcess(context);
    }

    private void calSupplyAndTheta(ShaleContext context) {
        for (Demand demand : context.getDemandList()) {
            double totalSupply = 0;
            for (Supply supply : demand.getSupplyList()) {
                totalSupply += supply.getTotal();
            }
            demand.setSupply(totalSupply);
            demand.setTheta(1.0 * demand.getQuantity() / demand.getSupply());
//            demand.setWeight(demand.getTheta());
        }
    }



    private void calBeta(ShaleContext context) {
        clearAlphaAndBeta(context);
        //构造库存->订单的映射关系
        calculateSatisfyDemandMap(context);
        int iterationCount = ShaleConstant.ITERATION_COUNT;
        for (int i = 0; i < iterationCount; i++) {
            for (Supply supply : context.getSupplyList()) {
                updateBeta(context, supply);
            }

            for (Demand demand : context.getDemandList()) {
                updateAlpha(demand);
            }
            printLog(context);
        }
    }

    private void printLog(ShaleContext context) {
        StringBuilder alphaBuilder = new StringBuilder("stage one beta ---> [");
        for (int i = 0; i < context.getSupplyList().size(); i++) {
            alphaBuilder.append("(").append(i).append(",").append(context.getSupplyList().get(i).getBeta()).append("),");
        }
        alphaBuilder.setCharAt(alphaBuilder.lastIndexOf(","), ']');
        System.out.println(alphaBuilder.toString());

        StringBuilder betaBuilder = new StringBuilder("stage one alpha ---> [");
        for (int i = 0; i < context.getDemandList().size(); i++) {
            betaBuilder.append("(").append(i).append(",").append(context.getDemandList().get(i).getAlpha()).append("),");
        }
        betaBuilder.setCharAt(betaBuilder.lastIndexOf(","), ']');
        System.out.println(betaBuilder.toString());
    }

    private void  clearAlphaAndBeta(ShaleContext context) {
        context.getDemandList().forEach(demand -> {
            demand.setAlpha(0);
        });
        context.getSupplyList().forEach(supply -> {
            supply.setBeta(0);
        });
    }

    private void calculateSupplyList(ShaleContext context) {
        for (Demand demand : context.getDemandList()) {
            List<Supply> list = new ArrayList<>();
            for (Supply Supply : context.getSupplyList()) {
                if (isSatisfyDemand(Supply, demand)) {
                    list.add(Supply);
                }
            }
            demand.setSupplyList(list);
        }
    }

    private void calculateSatisfyDemandMap(ShaleContext context) {
        for (Supply supply : context.getSupplyList()) {
            List<Demand> list = new ArrayList<>();
            for (Demand demand : context.getDemandList()) {
                if (isSatisfyDemand(supply, demand)) {
                    list.add(demand);
                }
                context.getSatisfyDemandMap().put(supply.getId(), list);
            }
        }
    }

    private boolean isSatisfyDemand(Supply supply, Demand demand) {
        Target supplyTarget = supply.getTarget();
        Target demandTarget = demand.getTarget();

        if (demandTarget.getGender() != null && !demandTarget.getGender().equals(supplyTarget.getGender())) {
            return false;
        }

        if (demandTarget.getCity() != null && !demandTarget.getCity().contains(supplyTarget.getCity())) {
            return false;
        }
        return true;
    }

    private void initContext(ShaleContext context) {
        context.getSupplyList().forEach(supply -> {
            supply.setRemain(supply.getTotal());
        });
        context.getSatisfyDemandMap().clear();
    }

    private void updateBeta(ShaleContext context, Supply supply) {
        List<Demand> demandList = context.getSatisfyDemandMap().get(supply.getId());
        if (CollectionUtils.isEmpty(demandList) || checkBeta0(demandList)) {
            supply.setBeta(0);
            return;
        }

        //先计算出β的范围，对任意订单demand都有 0 <= βi < Pj + Vj
        //简单处理，这里考虑第一个demand即可
        Demand demand0 = demandList.get(0);
        double minBeta = 0;
        double maxBeta = demand0.getWeight() + demand0.getPenalty();

        double start = minBeta;
        double end = maxBeta;

        Double result = null;
        Double totalRatio = null;
        while (start <= end) {
            if (end - start <= ShaleConstant.RATIO_GAP) {
                break;
            }
            double tempBeta = (start + end) / 2;
            totalRatio = calTotalRatio(demandList, tempBeta);
            double diff = totalRatio - 1.0;
            if (diff < -ShaleConstant.RATIO_GAP) {
                end = tempBeta;
            } else if (diff > ShaleConstant.RATIO_GAP) {
                start = tempBeta;
            } else {
                result = tempBeta;
                break;
            }
        }

        if (null == result || result < 0.0) {
            supply.setBeta(0);
        } else {
            supply.setBeta(result);
        }
    }

    private double calTotalRatio(List<Demand> demandList, double tempBeta) {
        double totalRatio = 0.0;
        for (Demand demand : demandList) {
            totalRatio +=  ShaleUtil.gij(demand.getTheta(), demand.getAlpha()
                    , tempBeta, demand.getWeight());
        }
        return totalRatio;
    }

    /**
     * beta = 0 是否有解
     * @param demandList
     * @return
     */
    private boolean checkBeta0(List<Demand> demandList) {
        double totalRatio = calTotalRatio(demandList, 0);
        double diff = totalRatio - 1.0;
        if (Math.abs(diff) < ShaleConstant.RATIO_GAP) {
            return true;
        }
        return false;
    }

    private void updateAlpha(Demand demand) {
        List<Supply> supplyList = demand.getSupplyList();

        if (CollectionUtils.isEmpty(supplyList) || checkAlph0(demand, supplyList)) {
            demand.setAlpha(0);
            return;
        }

        //先计算出αj的范围，对任意订单demand都有 0 <= αj <= Pj。
        //简单处理，这里考虑第一个demand即可
        double minAlpha = 0;
        double maxAlpha = demand.getPenalty();

        double start = minAlpha;
        double end = maxAlpha;

        Double result = null;
        Double totalSupply = null;
        while (start <= end) {
            if (end - start <= ShaleConstant.RATIO_GAP) {
                break;
            }
            double tempAlpha = (start + end) / 2;

            totalSupply = calTotalSupply(demand, supplyList, tempAlpha);
            double diff = totalSupply - demand.getQuantity();
            if (diff < -ShaleConstant.QUANTITY_GAP) {
                start = tempAlpha;
            } else if (diff > ShaleConstant.QUANTITY_GAP) {
                end = tempAlpha;
            } else {
                result = tempAlpha;
                break;
            }
        }

        if (null == result) {
            demand.setAlpha(demand.getPenalty());
        } else {
            demand.setAlpha(result);
        }
    }

    private double calTotalSupply(Demand demand, List<Supply> supplyList, double tempAlpha) {
        double totalSupply = 0.0;
        for (Supply supply : supplyList) {
            totalSupply += supply.getTotal() * ShaleUtil
                    .gij(demand.getTheta(), tempAlpha, supply.getBeta(), demand.getWeight());
        }
        return totalSupply;
    }

    /**
     * alpha = 0 是否有解
     * @param demand
     * @param supplyList
     * @return
     */
    private boolean checkAlph0(Demand demand, List<Supply> supplyList) {
        double totalSupply = calTotalSupply(demand, supplyList, 0);
        double diff = totalSupply - demand.getQuantity();
        if (Math.abs(diff) < ShaleConstant.QUANTITY_GAP) {
            return true;
        }
        return false;
    }

    /**
     * 当天所有请求的单次占量,提供一个默认方法
     */
    public boolean allocate(ShaleContext context, boolean calBeta) {
        initContext(context);
        //使用theta (dj/Sj)进行排序
        if (ShaleConstant.ORDER_BY_THETA) {
            context.getDemandList().sort(new ShaleComparator());
        } else {
            context.getDemandList().sort(new HwmComparator());
        }

        if (calBeta) {
            calBeta(context);
        }

        for (Demand demand : context.getDemandList()) {
            boolean result = calZeta(demand);
            if (!result) {
                return result;
            }
            List<Supply> supplyList = demand.getSupplyList();
            for (Supply supply : supplyList) {
                double gij = ShaleUtil.gij(demand.getTheta(), demand.getZeta(), supply.getBeta(), demand.getWeight());
                supply.setRemain(supply.getRemain() - Math.min(supply.getRemain(), supply.getTotal() * gij));
            }
        }

        StringBuilder zetaBuilder = new StringBuilder("stage two zeta ---> [");
        for (int i = 0; i < context.getDemandList().size(); i++) {
            zetaBuilder.append("(").append(i).append(",").append(context.getDemandList().get(i).getZeta()).append("),");
        }
        zetaBuilder.setCharAt(zetaBuilder.lastIndexOf(","), ']');
        System.out.println(zetaBuilder.toString());
        return true;
    }

    private boolean calZeta(Demand demand) {
        List<Supply> supplyList = demand.getSupplyList();
        double quantity = demand.getQuantity();

        // 这里计算出shale算法的ζ因子，对任意订单demand都有-Vj <= ζ <= P + Vj/θ
        double start = -demand.getWeight();
        double end = demand.getPenalty() + demand.getWeight() / demand.getTheta();

        double tempZeta = end;
        Double result = null;
        Double supply = null;
        int times = 0;
        while (start <= end) {
            times++;
            if (times > ShaleConstant.BINARY_CAL_TIMES) {
                break;
            }

            supply = calTotalSupplyWithRemain(demand, supplyList, tempZeta);
            if (supply - quantity < -ShaleConstant.QUANTITY_GAP) {
                start = tempZeta;
            } else if (supply - quantity > ShaleConstant.QUANTITY_GAP) {
                end = tempZeta;
            } else {
                result = tempZeta;
                break;
            }
            tempZeta = (start + end) / 2.0;
        }
        if (result != null) {
            demand.setZeta(result);
            demand.setActualSupply(supply);
            return true;
        } else {
            demand.setZeta(Double.MAX_VALUE);
            demand.setActualSupply(calTotalSupplyWithRemain(demand, supplyList, Double.MAX_VALUE));
            return false;
        }
    }

    private double calTotalSupplyWithRemain(Demand demand, List<Supply> supplyList, double tempAlpha) {
        double totalSupply = 0.0;
        for (Supply supply : supplyList) {
            double pvRemain = Math.max(0L, supply.getRemain());
            totalSupply += Math.min(pvRemain, supply.getTotal() * ShaleUtil
                    .gij(demand.getTheta(), tempAlpha, supply.getBeta(), demand.getWeight()));
        }
        return totalSupply;
    }

    protected abstract void doProcess(ShaleContext context);

}
